package com.smy.maven.plugin;

import com.google.common.collect.LinkedHashMultimap;
import com.smy.tool.api.annotation.SmyApiField;
import com.smy.tool.api.annotation.SmyApiMethod;
import com.smy.tool.api.annotation.SmyApiService;
import com.smy.tool.api.domain.SmyApiFieldInfo;
import com.smy.tool.api.domain.SmyApiMethodInfo;
import com.smy.tool.api.domain.SmyApiServiceInfo;

import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugin.logging.SystemStreamLog;

import java.io.File;
import java.io.FileFilter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.net.URL;
import java.net.URLDecoder;
import java.util.*;

/**
 * Description:
 *
 * @author 
 * @since 2017/12/1.
 */
public class ReadApiInfo {

    private Log log = new SystemStreamLog();

    private ClassLoader classLoader;

    private static final Set<String> TYPES = new HashSet<String>() {

        {
            add("java.lang.Integer");
            add("java.lang.Double");
            add("java.lang.Float");
            add("java.lang.Long");
            add("java.lang.Short");
            add("java.lang.Byte");
            add("java.lang.Boolean");
            add("java.lang.Character");
            add("java.lang.String");
            add("int");
            add("double");
            add("long");
            add("short");
            add("byte");
            add("boolean");
            add("char");
            add("float");
        }
    };

    private List<Class> clazz;

    public ReadApiInfo(String packageName) {
        this.classLoader = Thread.currentThread().getContextClassLoader();
        clazz = this.readApiPackage(packageName);
    }

    public ReadApiInfo(String... serviceNames) {
        this.classLoader = Thread.currentThread().getContextClassLoader();
        clazz = this.readApiService(serviceNames);
    }

    public ReadApiInfo(ClassLoader classLoader, String packageName) {
        this.classLoader = classLoader;
        clazz = this.readApiPackage(packageName);
    }

    public ReadApiInfo(ClassLoader classLoader, String... serviceNames) {
        this.classLoader = classLoader;
        clazz = this.readApiService(serviceNames);
    }

    public List<SmyApiServiceInfo> loadApiInfo() {
        if (clazz == null || clazz.size() == 0) {
            System.err.println("No found some services!");
            return null;
        }
        return this.getApiInfo(clazz);
    }

    private List<Class> readApiService(String... serviceNames) {
        List<Class> clazz = new ArrayList<>();
        for (String serviceName : serviceNames) {
            try {
                Class cls = classLoader.loadClass(serviceName);
                if (cls.getAnnotation(SmyApiService.class) != null) {
                    clazz.add(cls);
                    log.info("扫描到接口=====:" + cls.getName());
                }
            } catch (Exception e) {
                log.error("Load service class exception!", e);
            }
        }
        return clazz;
    }

    private List<Class> readApiPackage(String packageName) {
        List<Class> clazz = new ArrayList<>();
        String packagePathDir = packageName.replace(".", "/");
        try {
            Enumeration<URL> dirs = classLoader.getResources(packagePathDir);
            while (dirs.hasMoreElements()) {
                URL url = dirs.nextElement();
                String protocol = url.getProtocol();
                if ("file".equalsIgnoreCase(protocol)) {
                    String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
                    findClassInPackageByFile(packageName, filePath, clazz);
                }
            }
        } catch (Exception e) {
            log.error("load class from package '" + packageName + "' Exception!", e);
        }
        return clazz;
    }

    /**
     * 在package对应的路径下找到所有的class
     *
     * @param packageName package名称
     * @param filePath    package对应的路径
     * @param clazzs      找到class以后存放的集合
     */
    private void findClassInPackageByFile(String packageName, String filePath, List<Class> clazzs) {
        File dir = new File(filePath);
        if (!dir.exists() || !dir.isDirectory()) {
            return;
        }
        // 在给定的目录下找到所有的文件，并且进行条件过滤
        File[] dirFiles = dir.listFiles(new FileFilter() {

            @Override
            public boolean accept(File file) {
                boolean acceptDir = file.isDirectory();
                boolean acceptClass = file.getName().endsWith("class");
                return acceptDir || acceptClass;
            }
        });

        for (File file : dirFiles) {
            if (file.isDirectory()) {
                findClassInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), clazzs);
            } else {
                String className = file.getName().substring(0, file.getName().length() - 6);
                try {
                    Class cls = classLoader.loadClass(packageName + "." + className);
                    if (cls.getAnnotation(SmyApiService.class) != null) {
                        clazzs.add(cls);
                        log.info("扫描到接口:" + cls.getName());
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private List<SmyApiServiceInfo> getApiInfo(List<Class> clazzes) {
        List<SmyApiServiceInfo> serviceInfoList = new ArrayList<>();
        for (Class clazz : clazzes) {
            SmyApiService smyApiService = (SmyApiService) clazz.getAnnotation(SmyApiService.class);
            if (smyApiService != null) {
                Method[] methods = clazz.getMethods();
                List<SmyApiMethodInfo> methodInfoList = new ArrayList<>(methods.length);
                for (Method method : methods) {
                    SmyApiMethod smyApiMethod = method.getAnnotation(SmyApiMethod.class);
                    if (smyApiMethod != null) {
                        SmyApiMethodInfo smyApiMethodInfo = SmyApiMethodInfo.builder().methodName(method.getName())
                                .methodDesc(smyApiMethod.description()).build();
                        
                        //入参
                        Class[] inputParamClass = method.getParameterTypes();
                        if (inputParamClass != null && inputParamClass.length > 0) {
                        	LinkedHashMultimap<String, List<SmyApiFieldInfo>> inputParamMap = LinkedHashMultimap.create();
                            Annotation[][] args = method.getParameterAnnotations();
                            for (int i = 0 ; i < inputParamClass.length ; i++) {
                            	Class parameterClass = inputParamClass[i];
                                List<SmyApiFieldInfo> fieldInfoList = this.buildParamInfo(parameterClass, args[i]);
                                inputParamMap.put(parameterClass.getName(), fieldInfoList);
                            }
                            smyApiMethodInfo.setInputParamMap(inputParamMap);
                        }
                        //出参
                        Type returnType = method.getGenericReturnType();
                        Class outputParamClass = method.getReturnType();
                        Map<String, List<SmyApiFieldInfo>> outParamMap = new HashMap<>();
                        List<SmyApiFieldInfo> fieldInfoList = new ArrayList<>();
                        if (returnType instanceof ParameterizedType) {
                            ParameterizedType parameterizedType = (ParameterizedType) method.getGenericReturnType();
                            fieldInfoList = this.buildParamInfo(outputParamClass, parameterizedType);
                        } else {
                            fieldInfoList = this.buildParamInfo(outputParamClass, new Annotation[]{});
                        }
                        outParamMap.put(outputParamClass.getName(), fieldInfoList);
                        smyApiMethodInfo.setOutputParamMap(outParamMap);
                        methodInfoList.add(smyApiMethodInfo);
                    }
                }
                serviceInfoList.add(SmyApiServiceInfo.builder().serviceName(clazz.getName())
                        .serviceDesc(smyApiService.description())
                        .serviceAuthor(smyApiService.author())
                        .methodInfoList(methodInfoList)
                        .build());
            }
        }
        return serviceInfoList;
    }

	private List<SmyApiFieldInfo> buildParamInfo(Class parameterClass, Annotation[] args) {
        List<SmyApiFieldInfo> fieldInfoList = new ArrayList<>();
        Field[] fields = parameterClass.getDeclaredFields();
        for (Field field : fields) {
            SmyApiFieldInfo smyApiFieldInfo = this.readField(field);
            if (smyApiFieldInfo != null) {
                fieldInfoList.add(smyApiFieldInfo);
            }
        }
        if(args != null && args.length > 0){
        	for (Annotation arg : args) {
        		if(arg instanceof SmyApiField){        			
	        		SmyApiFieldInfo smyApiFieldInfo = SmyApiFieldInfo.builder().fieldName(((SmyApiField)arg).name())
	                        .fieldDesc(((SmyApiField)arg).description()).isNotNull(((SmyApiField)arg).isNotNull())
	                        .fieldType(parameterClass.getName()).build();
	        		fieldInfoList.add(smyApiFieldInfo);
        		}
			}
        }
        return fieldInfoList;
    }

    private List<SmyApiFieldInfo> buildParamInfo(Class parameterClass, ParameterizedType parameterizedType) {
        List<SmyApiFieldInfo> fieldInfoList = new ArrayList<>();
        Field[] fields = parameterClass.getDeclaredFields();
        for (Field field : fields) {
            Class<?> fieldType = field.getType();
            SmyApiFieldInfo smyApiFieldInfo = this.readField(field);
            if (smyApiFieldInfo != null && parameterizedType != null) {
                Type[] types = parameterizedType.getActualTypeArguments();
                for (Type type : types) {
                    if (!TYPES.contains(fieldType.getName())) {
                        if (!(type instanceof ParameterizedType) && fieldType.isAssignableFrom((Class<?>) type)) {
                            smyApiFieldInfo.setSubFieldInfoList(this.buildParamInfo((Class) type, new Annotation[]{}));
                        } else if (type instanceof ParameterizedType) {
                            smyApiFieldInfo.setSubFieldInfoList(this.buildParamInfo((Class) ((ParameterizedType) type)
                                    .getActualTypeArguments()[0], new Annotation[]{}));
                        }
                    }
                }
                fieldInfoList.add(smyApiFieldInfo);
            }
        }
        return fieldInfoList;
    }

    private SmyApiFieldInfo readField(Field field) {
        SmyApiField smyApiField = field.getAnnotation(SmyApiField.class);
        if (smyApiField == null) {
            return null;
        }
        SmyApiFieldInfo smyApiFieldInfo = SmyApiFieldInfo.builder().fieldName(field.getName())
                .fieldDesc(smyApiField.description()).isNotNull(smyApiField.isNotNull()).build();
        Class<?> fieldType = field.getType();
        if (TYPES.contains(fieldType.getName())) {
            smyApiFieldInfo.setFieldType(fieldType.getSimpleName());
        } else {
            if (fieldType.isAssignableFrom(List.class)) {
                smyApiFieldInfo.setFieldType("Array");
            } else if (fieldType.isAssignableFrom(Set.class)) {
                smyApiFieldInfo.setFieldType("Array");
            } else if (fieldType.isAssignableFrom(Map.class)) {
                smyApiFieldInfo.setFieldType("Map");
            } else if (fieldType.isAssignableFrom(BigDecimal.class)) {
                smyApiFieldInfo.setFieldType("Number");
            } else {
                smyApiFieldInfo.setFieldType("Object");
            }
            if (ParameterizedType.class.isAssignableFrom(field.getGenericType().getClass())) {
                ParameterizedType pt = (ParameterizedType) field.getGenericType();
                Field[] fields = ((Class) pt.getActualTypeArguments()[0]).getDeclaredFields();
                List<SmyApiFieldInfo> subFieldInfo = new ArrayList<>();
                for (Field subField : fields) {
                    SmyApiFieldInfo subSmyApiFieldInfo = readField(subField);
                    if (subSmyApiFieldInfo != null) {
                        subFieldInfo.add(subSmyApiFieldInfo);
                    }
                }
                smyApiFieldInfo.setSubFieldInfoList(subFieldInfo);
            }
        }
        return smyApiFieldInfo;
    }

}
